Typy zagnieżdżone i techniki funkcyjne w Javie

Statyczne typy składowe, klasy wewnętrzne, lokalne, anonimowe oraz wyrażenia lambda


apohllo@agh.edu.pl

http://apohllo.pl/dydaktyka/programowanie-obiektowe

konsultacje: wtorek 15:30 - 18:00, pokój 4.61

Pytanie 1

mam jedno pytanie odnośnie interfejsu map: nie implementuje on interfejsu iterable, natomiast posiada widoki takie jak entrySet, keySet, values, które zwracają wszystkie jego elementy w jakiejś kolejności. Na stronie Oracle napisano "The order of a map is defined as the order in which the iterators on the map's collection views return their elements.". Czy znaczy to, że te metody tworzą jakieś kolekcję po to, żeby zwrócić nam pożądane wyniki? Czym różniłby się w przeciwnym wypadku collection view od zwykłego collection?

Pytanie 2

Czy brak odwzorowania klucza przez mapę jest tym samym co odwzorowanie klucza na wartość null? Dlaczego w interfejsie Map metoda get(Object key), w przypadku gdy klucz nie zostanie znaleziony, zwraca null, a nie rzuca wyjątek?

Warto zauważyć, że może być to mylące dla początkujących programistów Javy, którzy zamiast użyć metody containsKey(Object key) zastosują mniej czytelny warunek: map.get(key) != null. Ponadto, jeśli mapa nie odwzorowuje pewnych dwóch różnych kluczy foo i bar, to wyrażenie map.get(foo) == map.get(bar) jest prawdziwe.

Podobne przemyślenia dotyczą również metody indexOf(int ch) w klasie String, która zwraca "sztuczną" wartość -1, w przypadku gdy znak nie występuje w łańcuchu. Nieuważny programista może łatwo zapomnieć o sprawdzeniu tego przypadku. Co więcej, na zwrócona wartość -1 może zostać wykorzystana w wyrażeniu arytmetycznym (np. ...+1, któremu odpowiada przejście o jeden znak w prawo). A to może doprowadzić do niepożądanego zachowania implementowanej funkcjonalności.

Ciekawi mnie również, dlaczego metoda substring(int beginIndex, int endIndex) w klasie String ma taką nieintuicyjną sygnaturę, zamiast substring(int beginIndex, int length).

Plan

  • statyczne typy składowe: interfejsy, wyliczenia, adnotacje
  • klasy wewnętrzne
  • klasy lokalne
  • klasy anonimowe
  • wyrażenia lambda
  • interfejs Stream

Statyczne typy składowe (zagnieżdżone) - interfejs wewnętrzny

In [7]:
class LinkedStack {
    static interface Linkable {
        public Linkable getNext();
        public void setNext(Linkable next);
    }
    
    private Linkable head;
    
    public void push(Linkable node) {
        Objects.requireNonNull(node, "Stack element cannot be null!");
        node.setNext(head);
        this.head = node;
    }
    
    public Linkable pop() {
        Linkable result = this.head;
        if(result != null){
            this.head = result.getNext();
        }
        return result;
    }
}
In [ ]:
class LinkableInteger implements LinkedStack.Linkable {
    private int value;
    
    private LinkedStack.Linkable next;
    
    public LinkableInteger(int i) { this.value = i; }
    
    public LinkedStack.Linkable getNext() { return next; }
    
    public void setNext(LinkedStack.Linkable next) { this.next = next; }
    
    public int getValue() { return this.value; }
}
In [ ]:
import static java.lang.System.out;

var stack = new LinkedStack();
stack.push(new LinkableInteger(1));
var linkedInteger = (LinkableInteger) stack.pop();
out.println(linkedInteger.getValue());
In [ ]:
stack.push(null);

Własności statycznego typu składowego

  • dostęp do prywatnych statycznych składowych typu otaczającego
  • dostęp typu otaczającego do prywatnych składowych typu statycznego
  • brak dostępu statycznego typu do składowych instacyjnych typu otaczającego

Map.Entry - przykład statycznego zagnieżdżonego interfejsu

In [ ]:
Map<String,Integer> numbers = new HashMap<>();
numbers.put("jeden", 1);
numbers.put("dwa", 2);
numbers.put("trzy", 3);
for(Map.Entry<String,Integer> entry : numbers.entrySet()){
    System.out.println("" + entry.getKey() + " : " + entry.getValue());
}

Instancyjne typy składowe - klasa wewnętrzna

In [ ]:
class BoundedArrayList {
    protected Object[] array;
    protected int pointer = 0;
    
    public BoundedArrayList(int size){
        array = new Object[size];
    }
    
    public boolean add(Object element){
        if(pointer < array.length){
            array[pointer] = element;
            pointer++;
            return true;
        } else {
            return false;
        }
    }
}
In [ ]:
class BoundedArrayListWithIterator extends BoundedArrayList {
    protected class ForwardIterator implements Iterator {
        private int index = 0;
        public boolean hasNext(){
            return index < pointer;
        }
        public Object next(){
            if(index >= pointer){
                throw new NoSuchElementException();
            }
            Object result = array[index];
            index++;
            return result;
        }
    }
    
    public BoundedArrayListWithIterator(int size){
        super(size);
    }

    public Iterator forwardIterator(){
        return new ForwardIterator();
    }
}
In [ ]:
class BoundedArrayListWithBackwardIterator extends BoundedArrayListWithIterator {
    public class BackwardIterator implements Iterator {
        private int index = pointer-1;
        public boolean hasNext(){
            return index >= 0;
        }
        public Object next(){
            if(index < 0){
                throw new NoSuchElementException();
            }
            Object result = array[index];
            index--;
            return result;
        }
    }
    
    public BoundedArrayListWithBackwardIterator(int size){
        super(size);
    }

    public Iterator backwardIterator(){
        return new BackwardIterator();
    }
}
In [ ]:
var list = new BoundedArrayListWithBackwardIterator(10);

list.add("Ala");
list.add("ma");
list.add("kota");

Iterator iterator = list.forwardIterator();
while(iterator.hasNext()){
    System.out.println(iterator.next());
}

iterator = list.backwardIterator();
while(iterator.hasNext()){
    System.out.println(iterator.next());
}
In [ ]:
System.out.println(list.forwardIterator());
In [ ]:
new BoundedArrayListWithBackwardIterator.BackwardIterator();
In [ ]:
list.add("A");
list.add("Ania");
list.add("nie");
list.add("ma");
list.add("kota");
list.add(",");
list.add("ona");
list.add("ma");
list.add("psa");
In [ ]:
iterator = list.backwardIterator();
while(iterator.hasNext()){
    System.out.println(iterator.next());
}

Własności instancyjnego typu składowego

  • dostęp do prywatnych własności (atrybutów i metod) typu otaczającego
  • dostęp typu otaczającego do prywatnych własnościu typu składowego
  • klasa składowa nie może mieć takiej samej nazwy jak jakaś klasa nadrzędna lub pakiet
  • klasa składowa nie może zawierać składowych statycznych, z wyjątkiem wartości stałych

Klasy lokalne

In [ ]:
class LocalExample {
    public static interface IntHolder { int getValue(); }
    
    public void run(){
        IntHolder[] holders = new IntHolder[10];
        for(int i = 0; i < 10; i++){
            final int fi = i;
            class MyIntHolder implements IntHolder {
                public int getValue() { return fi; }
            }
            holders[i] = new MyIntHolder();
        }
        
        for(int i = 0; i < 10; i++){
            System.out.println(holders[i].getValue());
        }
    }
}
In [ ]:
new LocalExample().run();

Własności klas lokalnych

  • klasy lokalne mają dostęp do własności prywatnych klas otaczających
  • klasy lokalne mają dostep do finalnych zmiennych (inaczej stałych) lokalnych (w tym argumentów metod oraz wyjątków)
  • odwołanie do zmiennych lokalnych tworzy domknięcie (closure)
  • nazwa klasy lokalnej jest dostępna tylko w bloku, w którym jest ona zdefiniowana

Klasy anonimowe

In [ ]:
class BoundedArrayList {
    private Object[] array;
    private int pointer = 0;

    public Iterator backwardIterator(){
        return new Iterator() {
            private int index = pointer-1;
            public boolean hasNext(){
                return index >= 0;
            }
            public Object next(){
                if(index < 0){
                    throw new NoSuchElementException();
                }
                Object result = array[index];
                index--;
                return result;
            }
        };
    }
}

Comparator - przykład często wykorzystywanej klasy anonimowej

In [ ]:
class NumberCollection {
    private SortedSet<String> numbers;
    public NumberCollection(){
        numbers = new TreeSet<>(new Comparator<String>(){
            public int compare(String a, String b){
                return a.length() - b.length();
            }
        });
    }

    public boolean add(String number){
        return numbers.add(number);
    }

    public String toString(){
        return numbers.toString();
    }
}
In [ ]:
NumberCollection numbers = new NumberCollection();
numbers.add("1111");
numbers.add("1");
numbers.add("111111");
numbers.add("zz");

System.out.println(numbers);

"Instancja" klasy abstrakcyjnej

In [ ]:
abstract class AbstractClass {
}

AbstractClass abstractValue = new AbstractClass(){};

Wyrażenia lambda

In [ ]:
import java.io.*;

File dir = new File("/home/apohllo");

String[] fileList = dir.list(new FilenameFilter() {
    public boolean accept(File file, String fileName){
        return fileName.endsWith(".java");
    }
});
for(String s : fileList){
    System.out.println(s);
}

Wyrażenie lambda zamiast klasy anonimowej

In [ ]:
import java.io.*;

String[] fileList = new File("/home/apohllo").list((f,s) -> { return s.endsWith(".java"); });
for(String s : fileList){
    System.out.println(s);
}
In [ ]:
import java.io.*;

String[] fileList = new File("/home/apohllo").
    // można pominąć słowo return
    list((f,s) -> s.endsWith(".java"));

for(String s : fileList){
    System.out.println(s);
}
In [ ]:
Arrays.asList(new File("/home/apohllo").list((f,s) -> s.endsWith(".java"))).
    stream().forEach(System.out::println);

Interfejs Stream i techniki funkcyjne

  • interfejs Collection został rozszerzony o metodę stream
  • metoda ta została wprowadzona aby ograniczyć wsteczną niekompatybilność
  • metoda ta jest domyślna w interfejsie Collection
  • metoda zwraca elemet typu Stream
  • interfejs Stream umożliwia wykonywania metod funkcyjnych:
    • allMatch
    • anyMatch
    • collect
    • concat
    • count
    • distinct
    • empty
    • filter
    • findAny
    • findFirst
    • flatMap
    • forEach
    • map
    • itd.

fliter, map i collect

In [ ]:
import java.util.stream.*;

var numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream().filter(e -> e % 2 == 0).
                 map(e -> e.toString()).
                 collect(Collectors.toList());

map

In [ ]:
import java.util.stream.*;

var numbers = Arrays.asList("jeden", "dwa", "trzy", "cztery");
numbers.stream().map(String::length).map(Object::toString).collect(Collectors.joining(", "));

forEach

In [ ]:
var numbers = Arrays.asList("jeden", "dwa", "trzy", "cztery");
numbers.stream().forEach(System.out::println);

map i reduce

In [ ]:
var numbers = Arrays.asList("jeden", "dwa", "trzy", "cztery");
double sum = numbers.stream().map(String::length).reduce(0, (x,y) -> { return x + y; });
System.out.println(sum / numbers.size());

Wartościowanie leniwe

In [ ]:
import java.util.function.*;
import java.util.stream.*;

public class SquareGenerator implements IntSupplier {
    private int current = 1;
    
    @Override
    public synchronized int getAsInt(){
        int thisResult = current * current;
        current++;
        return thisResult;
    }
}
In [ ]:
var squares = IntStream.generate(new SquareGenerator());
var stepThrough = squares.iterator();

for(int i = 0; i < 10; i++){
    System.out.println(stepThrough.nextInt());
}

System.out.println("-----------");

for(int i = 0; i < 10; i++){
    System.out.println(stepThrough.nextInt());
}
In [ ]:
var squares = IntStream.generate(new SquareGenerator());
squares.map(e -> (int) Math.sqrt(e)).limit(20).forEach(System.out::println);

Wyjątki

In [ ]:
import static java.lang.System.out;

public class StringConverter {
    public int convertToInt(String value) throws Exception {
        return Integer.parseInt(value);
    }
}

var converter = new StringConverter();
Arrays.asList("10", "zz").stream().
    forEach((element) -> { out.println(converter.convertToInt(element)); });
In [ ]:
public class StringConverter {
    public int convertToInt(String value) throws Exception {
        return Integer.parseInt(value);
    }
}

StringConverter converter = new StringConverter();
Arrays.asList("10", "zz", "20").stream().
    forEach((element) -> { 
        try {
            out.println(converter.convertToInt(element)); 
        } catch(Exception ex) {
            throw new RuntimeException(ex);
        }
    });
In [ ]:
try {
    Arrays.asList("10", "z", "20", "c").stream()
                .flatMap((element) -> {
                    try {
                        out.println(converter.convertToInt(element));
                        return null;
                    } catch (Exception ex) {
                        return Stream.of(new RuntimeException(ex));
                    }
                })
                .reduce((ex1, ex2) -> {
                    ex1.addSuppressed(ex2);
                    return ex1;
                })
                .ifPresent(ex -> {
                    throw ex;
                });
} catch(Exception ex) {
    ex.printStackTrace();
}
// Na podstawie: https://stackoverflow.com/questions/30117134/aggregate-runtime-exceptions-in-java-8-streams

Pytania?